{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# Introduction to Access Control\n",
    "\n",
    "We can now start using CrypTen to carry out private computations in some common use cases. In this tutorial, we will demonstrate how CrypTen would apply for the scenarios described in the Introduction. In all scenarios, we'll use a simple two-party setting and demonstrate how we can learn a linear SVM. In the process, we will see how access control works in CrypTen.\n",
    "\n",
    "As usual, we'll begin by importing the `crypten` and `torch` libraries, and initialize `crypten` with `crypten.init()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING:root:module 'torchvision.models.mobilenet' has no attribute 'ConvBNReLU'\n"
     ]
    }
   ],
   "source": [
    "import crypten\n",
    "import torch\n",
    "\n",
    "crypten.init()\n",
    "torch.set_num_threads(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setup\n",
    "In this tutorial, we will train a Linear SVM to perform binary classification. We will first generate 1000 ground truth samples using 100 features and a randomly generated hyperplane to separate positive and negative examples. \n",
    "\n",
    "(Note: this will cause our classes to be linearly separable, so a linear SVM will be able to classify with perfect accuracy given the right parameters.)\n",
    "\n",
    "We will also include a test set of examples (that are also linearly separable by the same hyperplane) to show that the model learns a general hyperplane rather than memorizing the training data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_features = 100\n",
    "num_train_examples = 1000\n",
    "num_test_examples = 100\n",
    "epochs = 40\n",
    "lr = 3.0\n",
    "\n",
    "# Set random seed for reproducibility\n",
    "torch.manual_seed(1)\n",
    "\n",
    "features = torch.randn(num_features, num_train_examples)\n",
    "w_true = torch.randn(1, num_features)\n",
    "b_true = torch.randn(1)\n",
    "\n",
    "labels = w_true.matmul(features).add(b_true).sign()\n",
    "\n",
    "test_features = torch.randn(num_features, num_test_examples)\n",
    "test_labels = w_true.matmul(test_features).add(b_true).sign()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we have generated our dataset, we will train our SVM in four different access control scenarios across two parties, Alice and Bob:\n",
    "\n",
    "- Data Labeling: Alice has access to features, while Bob has access to labels\n",
    "- Feature Aggregation: Alice has access to the first 50 features, while Bob has access to the last 50 features\n",
    "- Data Augmentation: Alice has access to the first 500 examples, while Bob has access to the last 500 examples\n",
    "- Model Hiding: Alice has access to `w_true` and `b_true`, while Bob has access to data samples to be classified\n",
    "\n",
    "Throughout this tutorial, we will assume Alice is using the rank 0 process, while Bob is using the rank 1 process. Additionally we will initialize our weights using random values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "ALICE = 0\n",
    "BOB = 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In each example, we will use the same code to train our linear SVM once the features and labels are properly encrypted. This code is contained in `examples/mpc_linear_svm`, but it is unnecessary to understand the training code to properly use access control. The training process itself is discussed in depth in later tutorials.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from examples.mpc_linear_svm.mpc_linear_svm import train_linear_svm, evaluate_linear_svm"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Saving / Loading Data\n",
    "\n",
    "We have now generated features and labels for our model to learn. In the scenarios we explore in this tutorial, we would like to ensure that each party only has access to some subset of the data we have generated. To do so, we will use special save / load methods that CrypTen provides to handle loading only to a specified party and synchronizing across processes. \n",
    "\n",
    "We will use `crypten.save_from_party()` here to save data from a particular source, then we will load using `crypten.load_from_party()` in each example to load on a particular source. The following code will save all data we will use to files, then each example will load its data as necessary.\n",
    "\n",
    "(Note that because we are operating on a single machine, all processes will have access to all of the files we are using. However, this still will work as expected when operating across machines.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[None, None]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from crypten import mpc\n",
    "\n",
    "# Specify file locations to save each piece of data\n",
    "filenames = {\n",
    "    \"features\": \"/tmp/features.pth\",\n",
    "    \"labels\": \"/tmp/labels.pth\",\n",
    "    \"features_alice\": \"/tmp/features_alice.pth\",\n",
    "    \"features_bob\": \"/tmp/features_bob.pth\",\n",
    "    \"samples_alice\": \"/tmp/samples_alice.pth\",\n",
    "    \"samples_bob\": \"/tmp/samples_bob.pth\",\n",
    "    \"w_true\": \"/tmp/w_true.pth\",\n",
    "    \"b_true\": \"/tmp/b_true.pth\",\n",
    "    \"test_features\": \"/tmp/test_features.pth\",\n",
    "    \"test_labels\": \"/tmp/test_labels.pth\",\n",
    "}\n",
    "\n",
    "\n",
    "@mpc.run_multiprocess(world_size=2)\n",
    "def save_all_data():\n",
    "    # Save features, labels for Data Labeling example\n",
    "    crypten.save(features, filenames[\"features\"])\n",
    "    crypten.save(labels, filenames[\"labels\"])\n",
    "    \n",
    "    # Save split features for Feature Aggregation example\n",
    "    features_alice = features[:50]\n",
    "    features_bob = features[50:]\n",
    "    \n",
    "    crypten.save_from_party(features_alice, filenames[\"features_alice\"], src=ALICE)\n",
    "    crypten.save_from_party(features_bob, filenames[\"features_bob\"], src=BOB)\n",
    "    \n",
    "    # Save split dataset for Dataset Aggregation example\n",
    "    samples_alice = features[:, :500]\n",
    "    samples_bob = features[:, 500:]\n",
    "    crypten.save_from_party(samples_alice, filenames[\"samples_alice\"], src=ALICE)\n",
    "    crypten.save_from_party(samples_bob, filenames[\"samples_bob\"], src=BOB)\n",
    "    \n",
    "    # Save true model weights and biases for Model Hiding example\n",
    "    crypten.save_from_party(w_true, filenames[\"w_true\"], src=ALICE)\n",
    "    crypten.save_from_party(b_true, filenames[\"b_true\"], src=ALICE)\n",
    "    \n",
    "    crypten.save_from_party(test_features, filenames[\"test_features\"], src=BOB)\n",
    "    crypten.save_from_party(test_labels, filenames[\"test_labels\"], src=BOB)\n",
    "    \n",
    "save_all_data()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Scenario 1: Data Labeling\n",
    "\n",
    "Our first example will focus on the <i>Data Labeling</i> scenario. In this example, Alice has access to features, while Bob has access to the labels. We will train our linear svm by encrypting the features from Alice and the labels from Bob, then training our SVM using an aggregation of the encrypted data.\n",
    "\n",
    "In order to indicate the source of a given encrypted tensor, we encrypt our tensor using `crypten.load()` (from a file) or `crypten.cryptensor()` (from a tensor) using a keyword argument `src`. This `src` argument takes the rank of the party we want to encrypt from (recall that ALICE is 0 and BOB is 1). \n",
    "\n",
    "(If the `src` is not specified, it will default to the rank 0 party. We will use the default when encrypting public values since the source is irrelevant in this case.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 0 --- Training Accuracy 53.40%\n",
      "Epoch 1 --- Training Accuracy 58.70%\n",
      "Epoch 2 --- Training Accuracy 63.80%\n",
      "Epoch 3 --- Training Accuracy 68.30%\n",
      "Epoch 4 --- Training Accuracy 73.60%\n",
      "Epoch 5 --- Training Accuracy 78.00%\n",
      "Epoch 6 --- Training Accuracy 81.00%\n",
      "Epoch 7 --- Training Accuracy 84.60%\n",
      "Epoch 8 --- Training Accuracy 87.00%\n",
      "Epoch 9 --- Training Accuracy 90.40%\n",
      "Epoch 10 --- Training Accuracy 91.50%\n",
      "Epoch 11 --- Training Accuracy 92.90%\n",
      "Epoch 12 --- Training Accuracy 93.80%\n",
      "Epoch 13 --- Training Accuracy 94.30%\n",
      "Epoch 14 --- Training Accuracy 95.50%\n",
      "Epoch 15 --- Training Accuracy 95.80%\n",
      "Epoch 16 --- Training Accuracy 96.30%\n",
      "Epoch 17 --- Training Accuracy 96.60%\n",
      "Epoch 18 --- Training Accuracy 96.70%\n",
      "Epoch 19 --- Training Accuracy 97.40%\n",
      "Epoch 20 --- Training Accuracy 98.30%\n",
      "Epoch 21 --- Training Accuracy 98.00%\n",
      "Epoch 22 --- Training Accuracy 98.10%\n",
      "Epoch 23 --- Training Accuracy 98.00%\n",
      "Epoch 24 --- Training Accuracy 98.70%\n",
      "Epoch 25 --- Training Accuracy 98.70%\n",
      "Epoch 26 --- Training Accuracy 99.30%\n",
      "Epoch 27 --- Training Accuracy 99.70%\n",
      "Epoch 28 --- Training Accuracy 99.60%\n",
      "Epoch 29 --- Training Accuracy 99.50%\n",
      "Epoch 30 --- Training Accuracy 99.70%\n",
      "Epoch 31 --- Training Accuracy 99.50%\n",
      "Epoch 32 --- Training Accuracy 99.60%\n",
      "Epoch 33 --- Training Accuracy 99.80%\n",
      "Epoch 34 --- Training Accuracy 100.00%\n",
      "Epoch 35 --- Training Accuracy 100.00%\n",
      "Epoch 36 --- Training Accuracy 100.00%\n",
      "Epoch 37 --- Training Accuracy 100.00%\n",
      "Epoch 38 --- Training Accuracy 100.00%\n",
      "Epoch 39 --- Training Accuracy 100.00%\n",
      "Test accuracy 92.00%\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[None, None]"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from crypten import mpc\n",
    "\n",
    "@mpc.run_multiprocess(world_size=2)\n",
    "def data_labeling_example():\n",
    "    \"\"\"Apply data labeling access control model\"\"\"\n",
    "    # Alice loads features, Bob loads labels\n",
    "    features_enc = crypten.load_from_party(filenames[\"features\"], src=ALICE)\n",
    "    labels_enc = crypten.load_from_party(filenames[\"labels\"], src=BOB)\n",
    "    \n",
    "    # Execute training\n",
    "    w, b = train_linear_svm(features_enc, labels_enc, epochs=epochs, lr=lr)\n",
    "    \n",
    "    # Evaluate model\n",
    "    evaluate_linear_svm(test_features, test_labels, w, b)\n",
    "        \n",
    "data_labeling_example()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Scenario 2: Feature Aggregation\n",
    "\n",
    "Next, we'll show how we can use CrypTen in the <i>Feature Aggregation</i> scenario. Here Alice and Bob each have 50 features for each sample, and would like to use their combined features to train a model. As before, Alice and Bob wish to keep their respective data private. This scenario can occur when multiple parties measure different features of a similar system, and their measurements may be proprietary or otherwise sensitive.\n",
    "\n",
    "Unlike the last scenario, one of our variables is split among two parties. This means we will have to concatenate the tensors encrypted from each party before passing them to the training code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 0 --- Training Accuracy 53.40%\n",
      "Epoch 1 --- Training Accuracy 58.70%\n",
      "Epoch 2 --- Training Accuracy 63.80%\n",
      "Epoch 3 --- Training Accuracy 68.30%\n",
      "Epoch 4 --- Training Accuracy 73.60%\n",
      "Epoch 5 --- Training Accuracy 78.00%\n",
      "Epoch 6 --- Training Accuracy 81.00%\n",
      "Epoch 7 --- Training Accuracy 84.60%\n",
      "Epoch 8 --- Training Accuracy 87.00%\n",
      "Epoch 9 --- Training Accuracy 90.40%\n",
      "Epoch 10 --- Training Accuracy 91.50%\n",
      "Epoch 11 --- Training Accuracy 92.90%\n",
      "Epoch 12 --- Training Accuracy 93.80%\n",
      "Epoch 13 --- Training Accuracy 94.40%\n",
      "Epoch 14 --- Training Accuracy 95.30%\n",
      "Epoch 15 --- Training Accuracy 96.30%\n",
      "Epoch 16 --- Training Accuracy 96.20%\n",
      "Epoch 17 --- Training Accuracy 96.80%\n",
      "Epoch 18 --- Training Accuracy 96.80%\n",
      "Epoch 19 --- Training Accuracy 97.20%\n",
      "Epoch 20 --- Training Accuracy 97.90%\n",
      "Epoch 21 --- Training Accuracy 97.80%\n",
      "Epoch 22 --- Training Accuracy 98.00%\n",
      "Epoch 23 --- Training Accuracy 98.90%\n",
      "Epoch 24 --- Training Accuracy 99.20%\n",
      "Epoch 25 --- Training Accuracy 99.40%\n",
      "Epoch 26 --- Training Accuracy 99.30%\n",
      "Epoch 27 --- Training Accuracy 99.50%\n",
      "Epoch 28 --- Training Accuracy 99.30%\n",
      "Epoch 29 --- Training Accuracy 99.30%\n",
      "Epoch 30 --- Training Accuracy 99.30%\n",
      "Epoch 31 --- Training Accuracy 99.50%\n",
      "Epoch 32 --- Training Accuracy 99.60%\n",
      "Epoch 33 --- Training Accuracy 100.00%\n",
      "Epoch 34 --- Training Accuracy 100.00%\n",
      "Epoch 35 --- Training Accuracy 100.00%\n",
      "Epoch 36 --- Training Accuracy 100.00%\n",
      "Epoch 37 --- Training Accuracy 100.00%\n",
      "Epoch 38 --- Training Accuracy 100.00%\n",
      "Epoch 39 --- Training Accuracy 100.00%\n",
      "Test accuracy 92.00%\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[None, None]"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@mpc.run_multiprocess(world_size=2)\n",
    "def feature_aggregation_example():\n",
    "    \"\"\"Apply feature aggregation access control model\"\"\"\n",
    "    # Alice loads some features, Bob loads other features\n",
    "    features_alice_enc = crypten.load_from_party(filenames[\"features_alice\"], src=ALICE)\n",
    "    features_bob_enc = crypten.load_from_party(filenames[\"features_bob\"], src=BOB)\n",
    "    \n",
    "    # Concatenate features\n",
    "    features_enc = crypten.cat([features_alice_enc, features_bob_enc], dim=0)\n",
    "    \n",
    "    # Encrypt labels\n",
    "    labels_enc = crypten.cryptensor(labels)\n",
    "    \n",
    "    # Execute training\n",
    "    w, b = train_linear_svm(features_enc, labels_enc, epochs=epochs, lr=lr)\n",
    "    \n",
    "    # Evaluate model\n",
    "    evaluate_linear_svm(test_features, test_labels, w, b)\n",
    "        \n",
    "feature_aggregation_example()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Scenario 3: Dataset Augmentation\n",
    "\n",
    "The next example shows how we can use CrypTen in a <i>Data Augmentation</i> scenario. Here Alice and Bob each have 500 samples, and would like to learn a classifier over their combined sample data. This scenario can occur in applications where several parties may each have access to a small amount of sensitive data, where no individual party has enough data to train an accurate model.\n",
    "\n",
    "Like the last scenario, one of our variables is split amongst parties, so we will have to concatenate tensors from encrypted from different parties. The main difference from the last scenario is that we are concatenating over the other dimension (the sample dimension rather than the feature dimension)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 0 --- Training Accuracy 53.40%\n",
      "Epoch 1 --- Training Accuracy 58.70%\n",
      "Epoch 2 --- Training Accuracy 63.80%\n",
      "Epoch 3 --- Training Accuracy 68.30%\n",
      "Epoch 4 --- Training Accuracy 73.60%\n",
      "Epoch 5 --- Training Accuracy 78.00%\n",
      "Epoch 6 --- Training Accuracy 81.00%\n",
      "Epoch 7 --- Training Accuracy 84.60%\n",
      "Epoch 8 --- Training Accuracy 87.00%\n",
      "Epoch 9 --- Training Accuracy 90.40%\n",
      "Epoch 10 --- Training Accuracy 91.50%\n",
      "Epoch 11 --- Training Accuracy 92.90%\n",
      "Epoch 12 --- Training Accuracy 93.80%\n",
      "Epoch 13 --- Training Accuracy 94.40%\n",
      "Epoch 14 --- Training Accuracy 95.30%\n",
      "Epoch 15 --- Training Accuracy 96.30%\n",
      "Epoch 16 --- Training Accuracy 96.20%\n",
      "Epoch 17 --- Training Accuracy 96.80%\n",
      "Epoch 18 --- Training Accuracy 96.80%\n",
      "Epoch 19 --- Training Accuracy 97.20%\n",
      "Epoch 20 --- Training Accuracy 97.90%\n",
      "Epoch 21 --- Training Accuracy 97.80%\n",
      "Epoch 22 --- Training Accuracy 98.00%\n",
      "Epoch 23 --- Training Accuracy 98.90%\n",
      "Epoch 24 --- Training Accuracy 99.20%\n",
      "Epoch 25 --- Training Accuracy 99.40%\n",
      "Epoch 26 --- Training Accuracy 99.50%\n",
      "Epoch 27 --- Training Accuracy 99.50%\n",
      "Epoch 28 --- Training Accuracy 99.00%\n",
      "Epoch 29 --- Training Accuracy 99.30%\n",
      "Epoch 30 --- Training Accuracy 99.30%\n",
      "Epoch 31 --- Training Accuracy 99.40%\n",
      "Epoch 32 --- Training Accuracy 99.50%\n",
      "Epoch 33 --- Training Accuracy 99.90%\n",
      "Epoch 34 --- Training Accuracy 99.80%\n",
      "Epoch 35 --- Training Accuracy 99.90%\n",
      "Epoch 36 --- Training Accuracy 99.90%\n",
      "Epoch 37 --- Training Accuracy 99.80%\n",
      "Epoch 38 --- Training Accuracy 99.90%\n",
      "Epoch 39 --- Training Accuracy 99.90%\n",
      "Test accuracy 92.00%\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[None, None]"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@mpc.run_multiprocess(world_size=2)\n",
    "def dataset_augmentation_example():\n",
    "    \"\"\"Apply dataset augmentation access control model\"\"\" \n",
    "    # Alice loads some samples, Bob loads other samples\n",
    "    samples_alice_enc = crypten.load_from_party(filenames[\"samples_alice\"], src=ALICE)\n",
    "    samples_bob_enc = crypten.load_from_party(filenames[\"samples_bob\"], src=BOB)\n",
    "    \n",
    "    # Concatenate features\n",
    "    samples_enc = crypten.cat([samples_alice_enc, samples_bob_enc], dim=1)\n",
    "    \n",
    "    labels_enc = crypten.cryptensor(labels)\n",
    "    \n",
    "    # Execute training\n",
    "    w, b = train_linear_svm(samples_enc, labels_enc, epochs=epochs, lr=lr)\n",
    "    \n",
    "    # Evaluate model\n",
    "    evaluate_linear_svm(test_features, test_labels, w, b)\n",
    "        \n",
    "dataset_augmentation_example()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Scenario 4: Model Hiding\n",
    "\n",
    "The last scenario we will explore involves <i>model hiding</i>. Here, Alice has a pre-trained model that cannot be revealed, while Bob would like to use this model to evaluate on private data sample(s). This scenario can occur when a pre-trained model is proprietary or contains sensitive information, but can provide value to other parties with sensitive data.\n",
    "\n",
    "This scenario is somewhat different from the previous examples because we are not interested in training the model. Therefore, we do not need labels. Instead, we will demonstrate this example by encrypting the true model parameters (`w_true` and `b_true`) from Alice and encrypting the test set from Bob for evaluation.\n",
    "\n",
    "(Note: Because we are using the true weights and biases used to generate the test labels, we will get 100% accuracy.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test accuracy 100.00%\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[None, None]"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@mpc.run_multiprocess(world_size=2)\n",
    "def model_hiding_example():\n",
    "    \"\"\"Apply model hiding access control model\"\"\"\n",
    "    # Alice loads the model\n",
    "    w_true_enc = crypten.load_from_party(filenames[\"w_true\"], src=ALICE)\n",
    "    b_true_enc = crypten.load_from_party(filenames[\"b_true\"], src=ALICE)\n",
    "    \n",
    "    # Bob loads the features to be evaluated\n",
    "    test_features_enc = crypten.load_from_party(filenames[\"test_features\"], src=BOB)\n",
    "    \n",
    "    # Evaluate model\n",
    "    evaluate_linear_svm(test_features_enc, test_labels, w_true_enc, b_true_enc)\n",
    "    \n",
    "model_hiding_example()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this tutorial we have reviewed four techniques where CrypTen can be used to perform encrypted training / inference. Each of these techniques can be used to facilitate computations in different privacy-preserving scenarios. However, these techniques can also be combined to increase the amount of scenarios where CrypTen can maintain privacy.\n",
    "\n",
    "For example, we can combine feature aggregation and data labeling to train a model on data split between three parties, where two parties each have access to a subset of features, and the third party has access to labels.\n",
    "\n",
    "Before exiting this tutorial, please clean up the files generated using the following code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "for fn in filenames.values():\n",
    "    if os.path.exists(fn): os.remove(fn)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
